﻿using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using IcicleFramework.Components;
using IcicleFramework.Components.Metadata;
using IcicleFramework.Entities;
using Microsoft.Xna.Framework;

namespace IcicleFramework.GameServices
{
    public class GameObjectManager : Dictionary<Guid, IGameObject>, IGameObjectManager
    {
        public void Initialize()
        {
            IGameObjectFactory factory = GameServiceManager.GetService<IGameObjectFactory>();

            if (factory != null)
                factory.OnGameObjectCreated += OnGameObjectCreated;
        }

        public void Update(GameTime gameTime) { }

        protected void OnGameObjectCreated(IGameObject newObject)
        {
            Add(newObject);
        }

        public void Add(IGameObject gameObject)
        {
            if (!this.ContainsKey(gameObject.GUID))
                this.Add(gameObject.GUID, gameObject);

            //Subscribe to the OnDestroyed event so we know when the IGameObject can be released.
            gameObject.OnDestroyed += OnGameObjectDestroyed;
        }

        public void OnGameObjectDestroyed(IGameObject gameObject)
        {
            //We should have this game object listed, but let's just check to be sure.
            //We also need to remember to unsubscribe from the OnDestroyed event.
            if (this.ContainsKey(gameObject.GUID))
            {
                gameObject.OnDestroyed -= OnGameObjectDestroyed;
                this.Remove(gameObject.GUID);
            }
        }

        public void Remove(IGameObject gameObject)
        {
            this.Remove(gameObject.GUID);
        }

        public ReadOnlyCollection<IGameObject> GetAll()
        {
            return new ReadOnlyCollection<IGameObject>(this.Values.ToList());
        }

        public List<IGameObject> FindAllWithMetadata(string metaName)
        {
            List<IGameObject> foundObjects = new List<IGameObject>();

            //For now, we have to scan through all of the game objects in order to figure out
            //which ones may or may not contain the given metadata.
            //A future improvement may be to store indices on metadata for quicker lookup.
            foreach (IGameObject gameObject in this.Values)
            {
                IMetadataComponent metadata = gameObject.GetComponent<IMetadataComponent>();

                if (metadata != null)
                {
                    if (metadata.HasMetadata(metaName))
                        foundObjects.Add(gameObject);
                }
            }
            
            return foundObjects;
        }

        public List<IGameObject> FindAllWithComponent<T>() where T : IBaseComponent
        {
            List<IGameObject> foundObjects = new List<IGameObject>();

            //Like searching for all game objects with a given metadata item, this requires us to look
            //at all of the gameobjects, and check if they have the given component.
            //A future improvement may be to store indices on components for quicker lookup.
            foreach (IGameObject gameObject in this.Values)
            {
                IBaseComponent component = gameObject.GetComponent<T>();

                //We found the component of type T within this game object!
                if (component != null)
                {
                    foundObjects.Add(gameObject);
                }
            }

            return foundObjects;
        }

        public IGameObject FindWithMetadata(string metaName)
        {
            IGameObject found = null;

            //In the worst case we'll have to loop through all game objects, but we'll break the loop short
            //when we find the first game object with the given metadata.
            //We could still improve this in the future with an index on metadata and simply grab the first listed game object there.
            foreach (IGameObject gameObject in this.Values)
            {
                IMetadataComponent metadata = gameObject.GetComponent<IMetadataComponent>();

                if (metadata != null)
                {
                    if (metadata.HasMetadata(metaName))
                    {
                        found = gameObject;
                        break;
                    }
                }
            }

            return found;
        }

        public IGameObject FindWithComponent<T>() where T : IBaseComponent
        {
            IGameObject found = null;

            foreach (IGameObject gameObject in this.Values)
            {
                IBaseComponent component = gameObject.GetComponent<T>();

                if (component != null)
                {
                    found = gameObject;
                    break;
                }
            }

            return found;
        }
    }
}
